package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	goruntime "runtime"
	"strings"
	"sync"
	"time"

	"github.com/anacrolix/torrent"
	"github.com/google/uuid"
	"github.com/wailsapp/wails/v2/pkg/runtime"
)

type TaskStatus string

const (
	StatusWaiting     TaskStatus = "waiting"
	StatusDownloading TaskStatus = "downloading"
	StatusPaused      TaskStatus = "paused"
	StatusCompleted   TaskStatus = "completed"
	StatusFailed      TaskStatus = "failed"
)

type Task struct {
	ID         string     `json:"id"`
	Name       string     `json:"name"`
	URL        string     `json:"url"`
	Type       string     `json:"type"` // http | bt
	Status     TaskStatus `json:"status"`
	Size       int64      `json:"size"`
	Downloaded int64      `json:"downloaded"`
	Speed      int64      `json:"speed"`
	ETA        int64      `json:"eta"` // seconds
	Category   string     `json:"category"`
	SavePath   string     `json:"savePath"`
	CreatedAt  int64      `json:"createdAt"`
	RetryCount int        `json:"retryCount"`
	Logs       []LogEntry `json:"-"` // 不导出到JSON，由专门的方法获取
	logMutex   sync.Mutex // 保护日志的互斥锁
}

// Settings holds user configurable preferences
type Settings struct {
    SavePath      string `json:"savePath"`
    MaxConcurrent int    `json:"maxConcurrent"`
    SpeedLimitKBs int64  `json:"speedLimitKBs"`
    Proxy         string `json:"proxy"`
    Lang          string `json:"lang"`
    MaxActive     int    `json:"maxActive"`
    RetryMax      int    `json:"retryMax"`
    BTKeepSeeding bool   `json:"btKeepSeeding"`
    BTListenPort  int    `json:"btListenPort"`
    BTTrackers    string `json:"btTrackers"` // newline-separated tracker list
}

// GlobalStats provides aggregate download statistics for UI display
type GlobalStats struct {
    Total     int   `json:"total"`
    Active    int   `json:"active"`
    Waiting   int   `json:"waiting"`
    Completed int   `json:"completed"`
    Failed    int   `json:"failed"`
    Speed     int64 `json:"speed"`
}

type Manager struct {
    ctx   context.Context
    mu    sync.Mutex
    tasks map[string]*Task
    // active download workers by task id
    workers map[string]*downloadWorker
    // defaults for downloader
    maxSegments     int   // per-task concurrent segments when starting fresh (0->fallback to 1)
    speedLimitB     int64 // bytes per second (0 = unlimited)
    defaultSavePath string
    proxy           string
    lang            string
    // scheduling
    maxActive   int // max concurrent active downloads
    activeCount int // current active downloads
    retryMax    int // max retry attempts per task
    // bt engine
    btClient      *torrent.Client
    btTorrents    map[string]*torrent.Torrent
    btKeepSeeding bool
    btListenPort  int
    // global trackers (raw text from settings, and parsed list)
    btTrackersRaw string
    btTrackers    []string
}

func NewManager() *Manager {
    return &Manager{
        tasks:          make(map[string]*Task),
        workers:        make(map[string]*downloadWorker),
        maxSegments:    4,
        speedLimitB:    0,
        defaultSavePath: "",
        proxy:          "",
        lang:           "zh-CN",
        maxActive:      2,
        activeCount:    0,
        retryMax:       3,
        btKeepSeeding:  false,
        btListenPort:   0,
    }
}

// parseBTTrackers parses a newline-separated tracker string into a de-duplicated list
func parseBTTrackers(s string) []string {
    lines := strings.Split(s, "\n")
    out := make([]string, 0, len(lines))
    seen := make(map[string]struct{})
    for _, ln := range lines {
        u := strings.TrimSpace(ln)
        if u == "" {
            continue
        }
        if !(strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "udp://")) {
            continue
        }
        if _, ok := seen[u]; ok {
            continue
        }
        seen[u] = struct{}{}
        out = append(out, u)
    }
    return out
}

// GetDefaultDownloadDir returns the OS-specific default Downloads directory for the current user
func (m *Manager) GetDefaultDownloadDir() string {
    return defaultDownloadDir()
}

// LogEntry 表示一条日志记录
type LogEntry struct {
	Time    time.Time `json:"time"`
	Level   string    `json:"level"` // "info", "warning", "error"
	Message string    `json:"message"`
}

// AddLog 添加一条日志记录
func (t *Task) AddLog(level, message string) {
	t.logMutex.Lock()
	defer t.logMutex.Unlock()

	// 限制日志条数，避免内存占用过大
	if len(t.Logs) >= 1000 {
		t.Logs = t.Logs[1:]
	}

	t.Logs = append(t.Logs, LogEntry{
		Time:    time.Now(),
		Level:   level,
		Message: message,
	})
}

// GetLogs 获取日志副本
func (t *Task) GetLogs() []LogEntry {
	t.logMutex.Lock()
	defer t.logMutex.Unlock()

	logs := make([]LogEntry, len(t.Logs))
	copy(logs, t.Logs)
	return logs
}

// GetTaskLogs 获取任务日志
func (m *Manager) GetTaskLogs(taskID string) ([]LogEntry, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	task, ok := m.tasks[taskID]
	if !ok {
		return nil, errors.New("task not found")
	}

	return task.GetLogs(), nil
}

// AddMagnet 添加磁力任务
func (m *Manager) AddMagnet(magnet, category string) (*Task, error) {
	if magnet == "" || !strings.HasPrefix(strings.ToLower(magnet), "magnet:") {
		return nil, errors.New("invalid magnet")
	}
	t := &Task{
		ID:        uuid.NewString(),
		Name:      "magnet",
		URL:       magnet,
		Type:      "bt",
		Category:  category,
		Status:    StatusWaiting,
		CreatedAt: time.Now().Unix(),
	}
	m.mu.Lock()
	m.tasks[t.ID] = t
	m.mu.Unlock()
	m.emitUpdate()
	m.schedule()
	return t, nil
}

// PickTorrentPath 选择 .torrent 文件
func (m *Manager) PickTorrentPath() (string, error) {
	if m.ctx == nil {
		return "", errors.New("no context")
	}
	path, err := runtime.OpenFileDialog(m.ctx, runtime.OpenDialogOptions{Filters: []runtime.FileFilter{{DisplayName: "Torrent", Pattern: "*.torrent"}}})
	if err != nil {
		return "", err
	}
	return path, nil
}

// AddTorrentPath 通过 .torrent 文件添加 BT 任务
func (m *Manager) AddTorrentPath(p string, category string) (*Task, error) {
	if p == "" {
		return nil, errors.New("empty path")
	}
	base := filepath.Base(p)
	t := &Task{
		ID:        uuid.NewString(),
		Name:      strings.TrimSuffix(base, filepath.Ext(base)),
		URL:       p,
		Type:      "bt",
		Category:  category,
		Status:    StatusWaiting,
		CreatedAt: time.Now().Unix(),
	}
	m.mu.Lock()
	m.tasks[t.ID] = t
	m.mu.Unlock()
	m.emitUpdate()
	m.schedule()
	return t, nil
}

// OpenInFolder 在系统文件管理器中显示任务文件
func (m *Manager) OpenInFolder(id string) error {
	m.mu.Lock()
	t, ok := m.tasks[id]
	m.mu.Unlock()
	if !ok {
		return errors.New("task not found")
	}
	if t.SavePath == "" {
		return errors.New("no save path")
	}
	target := t.SavePath
	switch goruntime.GOOS {
	case "windows":
		return exec.Command("explorer", "/select,", target).Start()
	case "darwin":
		return exec.Command("open", "-R", target).Start()
	default:
		return exec.Command("xdg-open", filepath.Dir(target)).Start()
	}
}

// Rename 重命名任务文件（同目录下），并更新任务名称与保存路径
func (m *Manager) Rename(id, newName string) error {
	if newName == "" {
		return errors.New("empty name")
	}
	m.mu.Lock()
	t, ok := m.tasks[id]
	// 不允许在下载中重命名
	if ok && t.Status == StatusDownloading {
		m.mu.Unlock()
		return errors.New("cannot rename while downloading")
	}
	m.mu.Unlock()
	if !ok {
		return errors.New("task not found")
	}
	if t.SavePath == "" {
		return errors.New("no save path")
	}
	dir := filepath.Dir(t.SavePath)
	// 生成唯一目标名
	base := newName
	if base == "" {
		base = t.Name
	}
	dst := filepath.Join(dir, base)
	// 若存在同名，追加 (n)
	if st, err := os.Stat(dst); err == nil && st.Mode().IsRegular() {
		ext := filepath.Ext(base)
		nameOnly := strings.TrimSuffix(base, ext)
		for i := 1; ; i++ {
			cand := fmt.Sprintf("%s(%d)%s", nameOnly, i, ext)
			try := filepath.Join(dir, cand)
			if _, err := os.Stat(try); os.IsNotExist(err) {
				dst = try
				break
			}
		}
	}
	if err := os.Rename(t.SavePath, dst); err != nil {
		return err
	}
	// 更新任务信息
	m.mu.Lock()
	t.SavePath = dst
	t.Name = filepath.Base(dst)
	m.mu.Unlock()
	m.emitUpdate()
	return nil
}

// Stats returns aggregate statistics across all tasks
func (m *Manager) Stats() (GlobalStats, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var gs GlobalStats
	for _, t := range m.tasks {
		gs.Total++
		switch t.Status {
		case StatusDownloading:
			gs.Active++
		case StatusWaiting, StatusPaused:
			gs.Waiting++
		case StatusCompleted:
			gs.Completed++
		case StatusFailed:
			gs.Failed++
		}

		// aggregate download speed
		if t.Speed > 0 {
			gs.Speed += t.Speed
		}
	}
	return gs, nil
}

func (m *Manager) PickSavePath(suggest string) (string, error) {
	if m.ctx == nil {
		return "", errors.New("no context")
	}
	dir := m.defaultSavePath
	if dir == "" {
		dir = defaultDownloadDir()
	}
	opts := runtime.SaveDialogOptions{
		DefaultFilename:  suggest,
		DefaultDirectory: dir,
	}
	path, err := runtime.SaveFileDialog(m.ctx, opts)
	if err != nil {
		return "", err
	}
	return path, nil
}

// startup: Wails 会在应用启动时调用
func (m *Manager) startup(ctx context.Context) {
	m.ctx = ctx
	// load settings from disk (best-effort)
	_ = m.loadSettings()
	// If no default save path is configured, default to user's Downloads directory
	if m.defaultSavePath == "" {
		m.defaultSavePath = defaultDownloadDir()
	}
	m.emitUpdate()
}



func (m *Manager) emitUpdate() {
	if m.ctx != nil {
		runtime.EventsEmit(m.ctx, "tasks:update")
	}
}

func (m *Manager) List() ([]*Task, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	out := make([]*Task, 0, len(m.tasks))
	for _, t := range m.tasks {
		copy := *t
		out = append(out, &copy)
	}
	return out, nil
}

func (m *Manager) ListByCategory(category string) ([]*Task, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	out := make([]*Task, 0)
	for _, t := range m.tasks {
		if category == "" || t.Category == category {
			copy := *t
			out = append(out, &copy)
		}
	}
	return out, nil
}

func (m *Manager) ListCategories() ([]string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	mset := map[string]struct{}{}
	for _, t := range m.tasks {
		if t.Category != "" {
			mset[t.Category] = struct{}{}
		}
	}
	out := make([]string, 0, len(mset))
	for k := range mset {
		out = append(out, k)
	}
	return out, nil
}

func (m *Manager) Add(rawurl string, savePath string) (*Task, error) {
	if rawurl == "" {
		return nil, errors.New("empty url")
	}
	u, err := url.Parse(rawurl)
	if err != nil {
		return nil, err
	}
	name := path.Base(u.Path)
	if name == "/" || name == "." || name == "" {
		name = rawurl
	}
	t := &Task{
		ID:        uuid.NewString(),
		Name:      name,
		URL:       rawurl,
		Type:      "http",
		Status:    StatusWaiting,
		CreatedAt: time.Now().Unix(),
		SavePath:  savePath,
	}
	m.mu.Lock()
	m.tasks[t.ID] = t
	m.mu.Unlock()
	m.emitUpdate()
	// schedule start
	m.schedule()
	return t, nil
}

func (m *Manager) AddWithOptions(rawurl string, savePath string, category string) (*Task, error) {
	t, err := m.Add(rawurl, savePath)
	if err != nil {
		return nil, err
	}
	m.mu.Lock()
	if savePath != "" {
		t.SavePath = savePath
	}
	if category != "" {
		t.Category = category
	}
	m.mu.Unlock()
	m.emitUpdate()
	// 尝试调度
	m.schedule()
	return t, nil
}

func (m *Manager) Pause(id string) error {
	m.mu.Lock()
	t, ok := m.tasks[id]
	if !ok {
		m.mu.Unlock()
		return errors.New("task not found")
	}
	// cancel running worker if any
	if m.workers != nil {
		if w, exists := m.workers[id]; exists && w != nil {
			w.cancel()
		}
	}
	if t.Status != StatusCompleted && t.Status != StatusFailed {
		t.Status = StatusPaused
	}
	m.mu.Unlock()
	m.emitUpdate()
	// 由 worker 退出时归还并发槽并重新调度
	return nil
}

func (m *Manager) Resume(id string) error {
	m.mu.Lock()
	t, ok := m.tasks[id]
	if !ok {
		m.mu.Unlock()
		return errors.New("task not found")
	}
	// 将任务放回等待队列，由调度器根据并发限额启动
	if t.Status == StatusPaused || t.Status == StatusFailed || t.Status == StatusWaiting {
		t.Status = StatusWaiting
	}
	m.mu.Unlock()
	m.emitUpdate()
	m.schedule()
	return nil
}

func (m *Manager) Remove(id string) error {
	// cancel if running
	m.mu.Lock()
	if m.workers != nil {
		if w, exists := m.workers[id]; exists && w != nil {
			w.cancel()
		}
	}
	if _, ok := m.tasks[id]; !ok {
		m.mu.Unlock()
		return errors.New("task not found")
	}
	delete(m.tasks, id)
	m.mu.Unlock()
	m.emitUpdate()
	return nil
}

// PauseAll pauses all active and waiting downloads
func (m *Manager) PauseAll() error {
	m.mu.Lock()
	defer m.mu.Unlock()

	// Cancel all active workers
	for _, w := range m.workers {
		if w != nil {
			w.cancel()
		}
	}

	// Update task statuses
	for _, t := range m.tasks {
		if t.Status == StatusDownloading || t.Status == StatusWaiting {
			t.Status = StatusPaused
		}
	}

	m.emitUpdate()
	return nil
}

// ResumeAll resumes all paused and failed downloads
func (m *Manager) ResumeAll() error {
	m.mu.Lock()
	// Update task statuses to waiting
	for _, t := range m.tasks {
		if t.Status == StatusPaused || t.Status == StatusFailed {
			t.Status = StatusWaiting
		}
	}
	m.mu.Unlock()

	m.emitUpdate()
	m.schedule() // Start as many as allowed by concurrency
	return nil
}

// ClearCompleted removes all completed downloads
func (m *Manager) ClearCompleted() error {
	m.mu.Lock()
	defer m.mu.Unlock()

	// Find all completed tasks
	var toDelete []string
	for id, t := range m.tasks {
		if t.Status == StatusCompleted {
			toDelete = append(toDelete, id)
		}
	}

	// Delete them
	for _, id := range toDelete {
		delete(m.tasks, id)
	}

	if len(toDelete) > 0 {
		m.emitUpdate()
	}
	return nil
}

// BrowseFolder 弹出系统目录选择对话框
func (m *Manager) BrowseFolder() (string, error) {
    // runtime.OpenDirectoryDialog 第一个参数是窗口，可传 nil 使用默认窗口
    dir := m.defaultSavePath
    if dir == "" {
        dir = defaultDownloadDir()
    }
    folder, err := runtime.OpenDirectoryDialog(m.ctx, runtime.OpenDialogOptions{
        Title: "选择保存目录",
        // 可以根据需要设置默认路径
        DefaultDirectory: dir,
    })
	if err != nil {
		return "", err
	}

	if folder == "" {
		return "", errors.New("未选择目录")
	}

	// 返回选择的目录路径
	return folder, nil
}

// settings persistence helpers
func (m *Manager) settingsDir() string {
	if dir, err := os.UserConfigDir(); err == nil && dir != "" {
		return filepath.Join(dir, "dgw")
	}
	if home, err := os.UserHomeDir(); err == nil && home != "" {
		return filepath.Join(home, ".dgw")
	}
	return "."
}

func (m *Manager) settingsPath() string {
	return filepath.Join(m.settingsDir(), "settings.json")
}

func (m *Manager) loadSettings() error {
	b, err := os.ReadFile(m.settingsPath())
	if err != nil {
		return err
	}
	var s Settings
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}
	m.mu.Lock()
	if s.SavePath != "" {
		m.defaultSavePath = s.SavePath
	}
	if s.MaxConcurrent > 0 {
		m.maxSegments = s.MaxConcurrent
	}
	if s.SpeedLimitKBs >= 0 {
		m.speedLimitB = s.SpeedLimitKBs * 1024
	}
	if s.Proxy != "" {
		m.proxy = s.Proxy
	}
	if s.Lang != "" {
		m.lang = s.Lang
	}
	if s.MaxActive > 0 {
		m.maxActive = s.MaxActive
	}
	if s.RetryMax > 0 {
		m.retryMax = s.RetryMax
	}
	m.btKeepSeeding = s.BTKeepSeeding
	if s.BTListenPort >= 0 {
		m.btListenPort = s.BTListenPort
	}
	m.btTrackersRaw = s.BTTrackers
	m.btTrackers = parseBTTrackers(s.BTTrackers)
	m.mu.Unlock()
	return nil
}

func (m *Manager) saveSettings() error {
	dir := m.settingsDir()
	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}
	m.mu.Lock()
	s := Settings{
		SavePath:      m.defaultSavePath,
		MaxConcurrent: m.maxSegments,
		SpeedLimitKBs: m.speedLimitB / 1024,
		Proxy:         m.proxy,
		Lang:          m.lang,
		MaxActive:     m.maxActive,
		RetryMax:      m.retryMax,
		BTKeepSeeding: m.btKeepSeeding,
		BTListenPort:  m.btListenPort,
		BTTrackers:    m.btTrackersRaw,
	}
	m.mu.Unlock()
	b, err := json.MarshalIndent(s, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(m.settingsPath(), b, 0644)
}

func (m *Manager) GetSettings() (Settings, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	return Settings{
		SavePath:      m.defaultSavePath,
		MaxConcurrent: m.maxSegments,
		SpeedLimitKBs: m.speedLimitB / 1024,
		Proxy:         m.proxy,
		Lang:          m.lang,
		MaxActive:     m.maxActive,
		RetryMax:      m.retryMax,
		BTKeepSeeding: m.btKeepSeeding,
		BTListenPort:  m.btListenPort,
		BTTrackers:    m.btTrackersRaw,
	}, nil
}

func (m *Manager) UpdateSettings(s Settings) error {
	m.mu.Lock()
	if s.SavePath != "" { m.defaultSavePath = s.SavePath }
	if s.MaxConcurrent > 0 { m.maxSegments = s.MaxConcurrent }
	if s.SpeedLimitKBs >= 0 { m.speedLimitB = s.SpeedLimitKBs * 1024 }
	if s.Proxy != "" { m.proxy = s.Proxy }
	if s.Lang != "" { m.lang = s.Lang }
	if s.MaxActive > 0 { m.maxActive = s.MaxActive }
	if s.RetryMax > 0 { m.retryMax = s.RetryMax }
	m.btKeepSeeding = s.BTKeepSeeding
	if s.BTListenPort >= 0 { m.btListenPort = s.BTListenPort }
	m.btTrackersRaw = s.BTTrackers
	m.btTrackers = parseBTTrackers(s.BTTrackers)
	m.mu.Unlock()
	_ = m.saveSettings()
	m.emitUpdate()
	return nil
}

// concurrency helpers and scheduler
func (m *Manager) decActive() {
	m.mu.Lock()
	if m.activeCount > 0 { m.activeCount-- }
	m.mu.Unlock()
}

func (m *Manager) onWorkerDone() {
	m.decActive()
	m.emitUpdate()
	m.schedule()
}

func (m *Manager) schedule() {
	m.mu.Lock()
	slots := m.maxActive - m.activeCount
	if slots <= 0 { m.mu.Unlock(); return }
	// collect waiting/paused
	candidates := make([]*Task, 0)
	for _, t := range m.tasks {
		if t.Status == StatusWaiting || t.Status == StatusPaused {
			candidates = append(candidates, t)
		}
	}
	// simple sort by CreatedAt (bubble)
	for i := 0; i < len(candidates); i++ {
		for j := i+1; j < len(candidates); j++ {
			if candidates[i].CreatedAt > candidates[j].CreatedAt {
				candidates[i], candidates[j] = candidates[j], candidates[i]
			}
		}
	}
	ids := []string{}
	for _, t := range candidates {
		if slots == 0 { break }
		ids = append(ids, t.ID)
		slots--
		m.activeCount++
		t.Status = StatusDownloading
	}
	m.mu.Unlock()
	for _, id := range ids {
		m.mu.Lock(); tt := m.tasks[id]; m.mu.Unlock()
		if tt != nil && tt.Type == "bt" {
			_ = m.startBT(id)
		} else {
			_ = m.startDownload(id)
		}
	}
}
